using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;

namespace XYZShell.CSHelper
{
    public class XMapper
    {

        public T Map<T>(object s)
        {
            var t = Activator.CreateInstance<T>();
            this.Map(s, t);
            return t;
        }
        public void Map<T>(object s, T t)
        {
            var methodName = getMethodName(s.GetType(), t.GetType());

            if (!methods.ContainsKey(methodName))
            {
                BuildClassMapMethod(s.GetType(), t.GetType());
            }
            methods[methodName].DynamicInvoke(s, t, this);
            // _mapper(s, t);
        }
        private Dictionary<string, Delegate> custom_methods = new Dictionary<string, Delegate>();
        public void AddMapper<S, T>(Func<S, T> mapper)
        {
            var methodName = getMethodName(typeof(S), typeof(T));
            custom_methods[methodName] = mapper;
        }
        internal T CustomMap<S, T>(S s)
        {
            var methodName = getMethodName(typeof(S), typeof(T));
            return ((Func<S, T>)custom_methods[methodName])(s);
        }
        public Action<S, T> GetAcMapper<S, T>()
        {
            var st = typeof(S);
            var tt = typeof(T);
            var methodName = getMethodName(st, tt);

            if (!methods.ContainsKey(methodName))
            {
                BuildClassMapMethod(st, tt);
            }
            if (!methods.ContainsKey(methodName))
            {
                throw new NotSupportedException("映射类型不支持");
            }
            return (S s, T t) => methods[methodName].DynamicInvoke(s, t, this);
        }

        public Func<S, T> GetMapper<S, T>()
        {
            var st = typeof(S);
            var tt = typeof(T);
            var methodName = getMethodName(st, tt);
            var ac = GetAcMapper<S, T>();
            Func<S, T> func = (S s) =>
            {
                var rs = Activator.CreateInstance<T>();
                ac(s, rs);
                return rs;
            };
            return func;
        }
        private bool IsValueType(Type type)
        {
            return type.IsValueType
                   || type == typeof(string)
                   || type == typeof(DateTime)
                   || type.IsEnum;
        }
        private bool isDicCanMap(Type source, Type target)
        {
            if (source.GenericTypeArguments.Length != 2)
            {
                return false;
            }

            if (source.GenericTypeArguments.Length != target.GenericTypeArguments.Length)
            {
                return false;
            }

            if (source.GenericTypeArguments[0] != target.GenericTypeArguments[0])
            {
                return false;
            }

            if (!typeof(IDictionary<,>).MakeGenericType(source.GenericTypeArguments).IsAssignableFrom(source))
            {
                return false;
            }

            if (!typeof(IDictionary<,>).MakeGenericType(target.GenericTypeArguments).IsAssignableFrom(target))
            {
                return false;
            }

            return true;
        }

        private bool isListCanMap(Type source, Type target)
        {
            if (!source.IsGenericType)
            {
                return false;
            }

            if (!typeof(IEnumerable<>).MakeGenericType(source.GenericTypeArguments).IsAssignableFrom(source))
            {
                return false;
            }

            if (!typeof(ICollection<>).MakeGenericType(target.GenericTypeArguments).IsAssignableFrom(target))
            {
                return false;
            }

            return true;
        }

        private Func<Type, Type, string> getMethodName =
            (Type source, Type target) => $"x{source.FullName}2{target.FullName}".Replace(".", "_");
        private Dictionary<string, MethodInfo> methodInfos = new Dictionary<string, MethodInfo>();
        private Dictionary<string, Delegate> methods = new Dictionary<string, Delegate>();


        internal static T MapValue<T>(T obj)
        {
            return obj;
        }
        internal static void MapValueList<T>(IEnumerable<T> source, ICollection<T> target)
        {
            if (source == null)
            {
                return;
            }
            foreach (var item in source)
            {
                target.Add(item);
            }
        }
        internal static void MapList<S, T>(IEnumerable<S> source, ICollection<T> target, Action<S, T> ac)
        {
            if (source == null)
            {
                return;
            }
            foreach (var item in source)
            {
                var t = Activator.CreateInstance<T>();
                ac(item, t);
                target.Add(t);
            }
        }
        private static T[] MapArray<S, T>(S[] source, Func<S, T> func)
        {
            if (source == null || source.Length == 0)
            {
                return new T[0];
            }
            var rs = new T[source.Length];
            for (int i = 0; i < source.Length; i++)
            {
                rs[i] = func(source[i]);
            }

            return rs;
        }

        private static void MapDic<K, S, T>(IDictionary<K, S> source, IDictionary<K, T> target, Func<S, T> mapper)
        {
            if (source == null)
            {
                return;
            }
            foreach (var item in source)
            {
                target[item.Key] = mapper(item.Value);
            }
        }

        private void BuildClassMapMethod(Type source, Type target)
        {
            var methodName = getMethodName(source, target);
            var method = new DynamicMethod(methodName, null, new Type[] { typeof(object), typeof(object), typeof(XMapper) }, typeof(XMapper));
            var dicSourceProp = new Dictionary<string, PropertyInfo>();
            methodInfos[methodName] = method;

            foreach (var propertyInfo in source.GetProperties())
            {
                dicSourceProp[propertyInfo.Name] = propertyInfo;
            }
            //ldarg_0是source
            //ldarg_1是target
            var il = method.GetILGenerator();

            var retlab = il.DefineLabel(); //定义返回位置标签  
            // if(source == null) {return null;}
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldnull);
            il.Emit(OpCodes.Ceq);
            il.Emit(OpCodes.Brfalse_S, retlab);

            il.Emit(OpCodes.Ret); // return;

            il.MarkLabel(retlab);
            // var s = arg0 as Source;
            // var t = arg1 as Target;
            var sourceObj = il.DeclareLocal(source);
            var targetObj = il.DeclareLocal(target);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Isinst, source);
            il.Emit(OpCodes.Stloc_S, sourceObj);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Isinst, target);
            il.Emit(OpCodes.Stloc_S, targetObj);

            foreach (var item in target.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (!dicSourceProp.ContainsKey(item.Name))
                {
                    continue;
                }

                var attr = item.GetCustomAttributes(typeof(SkipMapAttribute), true);
                if (attr != null && attr.Any())
                {
                    continue;
                }

                var sourceItem = dicSourceProp[item.Name];

                if (!(sourceItem.CanRead && item.CanWrite))
                {
                    continue;
                }
                var mapperName = getMethodName(sourceItem.PropertyType, item.PropertyType);
                if (custom_methods.ContainsKey(mapperName))
                {
                    var mapper = typeof(XMapper).GetMethod(nameof(XMapper.CustomMap), BindingFlags.Instance | BindingFlags.NonPublic)
                        .MakeGenericMethod(sourceItem.PropertyType, item.PropertyType);
                    var isnulllab = il.DefineLabel();
                    // if(source.xx == null) 跳过拷贝值
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Ceq);
                    il.Emit(OpCodes.Brtrue_S, isnulllab);

                    //target.obj = XMapper.CustomMap<T>(source.obj);
                    il.Emit(OpCodes.Ldloc_S, targetObj);
                    il.Emit(OpCodes.Ldarg_2);
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Callvirt, mapper);
                    il.Emit(OpCodes.Callvirt, item.SetMethod);
                    il.MarkLabel(isnulllab);
                }
                if (IsValueType(item.PropertyType) && sourceItem.PropertyType == item.PropertyType)
                {
                    if (sourceItem.PropertyType == item.PropertyType)
                    {
                        // target.item = source.item;
                        il.Emit(OpCodes.Ldloc_S, targetObj);
                        il.Emit(OpCodes.Ldloc_S, sourceObj);
                        il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                        il.Emit(OpCodes.Callvirt, item.SetMethod);
                    }
                }
                else if (isDicCanMap(sourceItem.PropertyType, item.PropertyType))
                {
                    var k = sourceItem.PropertyType.GenericTypeArguments[0];
                    var s = sourceItem.PropertyType.GenericTypeArguments[1];
                    var t = item.PropertyType.GenericTypeArguments[1];
                    var mapmethod = typeof(XMapper)
                        .GetMethod(nameof(XMapper.MapDic),
                            BindingFlags.Static | BindingFlags.NonPublic)
                        .MakeGenericMethod(k, s, t);
                    var mapperFuncType = typeof(Func<,>).MakeGenericType(s, t);
                    var dicvar = il.DeclareLocal(item.PropertyType);
                    var mapperFuncLoc = il.DeclareLocal(mapperFuncType);
                    il.Emit(OpCodes.Newobj, item.PropertyType.GetConstructor(Type.EmptyTypes));
                    il.Emit(OpCodes.Stloc_S, dicvar);

                    if (IsValueType(s) && s == t)
                    {
                        var mapper = typeof(XMapper)
                         .GetMethod(nameof(XMapper.MapValue),
                             BindingFlags.Static | BindingFlags.NonPublic)
                         .MakeGenericMethod(s);
                        il.Emit(OpCodes.Ldnull);
                        il.Emit(OpCodes.Ldftn, mapper);
                        il.Emit(OpCodes.Newobj, mapperFuncType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));
                        il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
                    }
                    else
                    {
                        var getMapper = typeof(XMapper).GetMethod(nameof(XMapper.GetMapper)).MakeGenericMethod(s, t);
                        il.Emit(OpCodes.Ldarg_2);
                        il.Emit(OpCodes.Callvirt, getMapper);
                        il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
                    }
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldloc_S, dicvar);
                    il.Emit(OpCodes.Ldloc_S, mapperFuncLoc);
                    il.Emit(OpCodes.Call, mapmethod);

                    il.Emit(OpCodes.Ldloc_S, targetObj);
                    il.Emit(OpCodes.Ldloc_S, dicvar);
                    il.Emit(OpCodes.Callvirt, item.SetMethod);
                }
                else if (isListCanMap(sourceItem.PropertyType, item.PropertyType))
                {
                    var targetArgType = item.PropertyType.GenericTypeArguments[0];
                    var sourceArgType = sourceItem.PropertyType.GenericTypeArguments[0];
                    var isnulllab = il.DefineLabel();
                    var targetListLocal = il.DeclareLocal(item.PropertyType);
                    // target.xx = null;
                    il.Emit(OpCodes.Ldloc_S, targetObj);
                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Callvirt, item.SetMethod);
                    // if(source.xx == null) 跳过拷贝值
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Ceq);
                    il.Emit(OpCodes.Brtrue_S, isnulllab);

                    // targetListLocal = new List<T>();

                    il.Emit(OpCodes.Newobj, item.PropertyType.GetConstructor(Type.EmptyTypes));
                    il.Emit(OpCodes.Stloc_S, targetListLocal);

                    //MapValueList(source.list, targetlistlocal)
                    //target.list = targetlistlocal;
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldloc_S, targetListLocal);
                    if (IsValueType(sourceArgType) && sourceArgType == targetArgType)
                    {
                        var mapvalueListFunc = typeof(XMapper).GetMethod(nameof(XMapper.MapValueList), BindingFlags.NonPublic | BindingFlags.Static)
                            .MakeGenericMethod(sourceArgType);
                        il.Emit(OpCodes.Call, mapvalueListFunc);
                    }
                    else
                    {
                        var actype = typeof(Action<,>).MakeGenericType(sourceArgType, targetArgType);
                        //var acLoc = il.DeclareLocal(actype);
                        var getMapper = typeof(XMapper).GetMethod(nameof(XMapper.GetAcMapper)).MakeGenericMethod(sourceArgType, targetArgType);
                        il.Emit(OpCodes.Ldarg_2);
                        il.Emit(OpCodes.Callvirt, getMapper);
                        //il.Emit(OpCodes.Stloc_S, acLoc);
                        var mapList = typeof(XMapper).GetMethod(nameof(XMapper.MapList), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(sourceArgType, targetArgType);
                        il.Emit(OpCodes.Call, mapList);
                    }

                    il.Emit(OpCodes.Ldloc_S, targetObj);
                    il.Emit(OpCodes.Ldloc_S, targetListLocal);
                    il.Emit(OpCodes.Callvirt, item.SetMethod);

                    il.MarkLabel(isnulllab);
                }
                else if (item.PropertyType.IsArray && sourceItem.PropertyType.IsArray)
                {
                    var targetelementType = item.PropertyType.GetElementType();
                    var sourceElementType = sourceItem.PropertyType.GetElementType();
                    var mapperFuncType = typeof(Func<,>).MakeGenericType(sourceElementType, targetelementType);
                    var mapperFuncLoc = il.DeclareLocal(mapperFuncType);

                    if (IsValueType(targetelementType) && targetelementType == sourceElementType)
                    {
                        var mapper = typeof(XMapper)
                         .GetMethod(nameof(XMapper.MapValue),
                             BindingFlags.Static | BindingFlags.NonPublic)
                         .MakeGenericMethod(targetelementType);
                        il.Emit(OpCodes.Ldnull);
                        il.Emit(OpCodes.Ldftn, mapper);
                        il.Emit(OpCodes.Newobj, mapperFuncType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));
                        il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
                    }
                    else
                    {
                        var getMapper = typeof(XMapper).GetMethod(nameof(XMapper.GetMapper)).MakeGenericMethod(sourceElementType, targetelementType);
                        il.Emit(OpCodes.Ldarg_2);
                        il.Emit(OpCodes.Callvirt, getMapper);
                        il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
                    }
                    //*
                    var maplistmethod = typeof(XMapper).GetMethod(nameof(XMapper.MapArray),
                            BindingFlags.Static | BindingFlags.NonPublic)
                        .MakeGenericMethod(new Type[] { sourceElementType, targetelementType });
                    il.Emit(OpCodes.Ldloc_S, targetObj);
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldloc_S, mapperFuncLoc);
                    il.Emit(OpCodes.Call, maplistmethod);
                    il.Emit(OpCodes.Callvirt, item.SetMethod);
                    //*/
                }
                else if (sourceItem.PropertyType.IsClass && item.PropertyType.IsClass)
                {
                    //* var targetPropClass = new TargetPropClass(); 
                    //   xSourceClass2TargetClass(source.class, targetPropClass);
                    // target.class = targetPropClass;
                    var cmethodname = getMethodName(sourceItem.PropertyType, item.PropertyType);
                    if (!methodInfos.ContainsKey(cmethodname))
                    {
                        BuildClassMapMethod(sourceItem.PropertyType, item.PropertyType);
                    }
                    var cmethod = methodInfos[cmethodname];
                    var retnull = il.DefineLabel();
                    var tmpLocal = il.DeclareLocal(item.PropertyType);
                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Ceq);
                    il.Emit(OpCodes.Brtrue_S, retnull);

                    il.Emit(OpCodes.Newobj, item.PropertyType.GetConstructor(Type.EmptyTypes));
                    il.Emit(OpCodes.Stloc_S, tmpLocal);

                    il.Emit(OpCodes.Ldloc_S, sourceObj);
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Ldloc_S, tmpLocal);
                    il.Emit(OpCodes.Ldarg_2);
                    il.Emit(OpCodes.Call, cmethod);

                    il.Emit(OpCodes.Ldloc_S, targetObj);
                    il.Emit(OpCodes.Ldloc_S, tmpLocal);
                    il.Emit(OpCodes.Callvirt, item.SetMethod);

                    il.MarkLabel(retnull);
                    //*/
                }
            }
            il.Emit(OpCodes.Ret);

            methods[methodName] = method.CreateDelegate(typeof(Action<,,>).MakeGenericType(typeof(object), typeof(object), typeof(XMapper)));
        }

        public static XMapper Default { get; } = new XMapper();
    }
}
